#include <string.h>
#include <stdio.h>
#include <assert.h>

#include "interface.h"
#include "data.h"
#include "game.h"
#include "maze_csv.h"

unsigned char MAZE_DATA[1080];

static int BONUS_DATA[8][3] = {
    // { palette, points }
    /* cherry */        { 20, 100 },
    /* strawberry */    { 15, 300 },
    /* peach */    { 21, 500 },
    /* bell */    { 22, 3000 },
    /* apple */    { 20, 700 },
    /* grapes */    { 23, 1000 },
    /* galaxian */    { 9, 2000 },
    /* key */    { 22, 5000 },
};

static int get_wave_length(Game* game, int level_index, int wave) {
    static float SCATTER_WAVE_TABLE[3][7] = {
        // level 1
        { 7.0f, 20.0f, 7.0f, 20.0f, 5.0f, 20.0f, 5.0f },

        // levels 2-4
        { 7.0f, 20.0f, 7.0f, 20.0f, 5.0f, 1033.0f, 1.0f / 60.0f },

        // levels 5+
        { 7.0f, 20.0f, 7.0f, 20.0f, 5.0f, 1037.0f, 1.0f / 60.0f },
    };

    float* scatter_wave_table = NULL;
            
    if (game->level_index == 0) {
        scatter_wave_table = SCATTER_WAVE_TABLE[0];
    } else if (game->level_index >= 1 && game->level_index <= 3) {
        scatter_wave_table = SCATTER_WAVE_TABLE[1];
    } else if (game->level_index >= 5) {
        scatter_wave_table = SCATTER_WAVE_TABLE[2];
    }

    return (int)(scatter_wave_table[wave] * 60.0f);
}

void load_map(Game* game) {
    GameInterface* interface = game->interface;
    game->dots_left = 0;

    int i = 0;
    int x = 0;
    int y = 0;
    int num, read;

    while (i < sizeof(MAZE_CSV)) {
        if (MAZE_CSV[i] == ',') i++;
        else if (MAZE_CSV[i] == '\n') {
            i++;
            x = 0;
            y++;
        }
        else if (MAZE_CSV[i] == '\r' && MAZE_CSV[i+1] == '\n') {
            i += 2;
            x = 0;
            y++;
        }

        sscanf((const char*) MAZE_CSV + i, "%i%n", &num, &read);
        i += read;

        // write to tilemap
        game->map[x][y] = GAME_TILE_EMPTY;
        if (num >= 0) {
            assert(x >= 0 && y >= 0 && x < TILE_WIDTH && y < TILE_HEIGHT);
            interface->display_info.tile_palettes[x][y] = 27;

            // pellet
            if (num == 44) {
                interface->display_info.tiles[x][y] = CHAR_PELLET;
                game->map[x][y] = GAME_TILE_PELLET;
                game->dots_left++;
            
            // power pellet
            } else if (num == 45) {
                interface->display_info.tiles[x][y] = CHAR_POWER_PELLET;
                game->map[x][y] = GAME_TILE_POWER_PELLET;
                game->dots_left++;
                
            // normal tile
            } else {
                interface->display_info.tiles[x][y] = 208 + num;
                game->map[x][y] = GAME_TILE_SOLID;
            }
        }

        x++;
    }

    // ghost house door
    game->map[13][15] = GAME_TILE_SOLID;
    game->map[14][15] = GAME_TILE_SOLID;
    interface->display_info.tiles[13][15] = 206;
    interface->display_info.tiles[14][15] = 207;
    interface->display_info.tile_palettes[13][15] = 29;
    interface->display_info.tile_palettes[14][15] = 29;
}

uint8_t game_get_random_number(Game* game) {
    game->rng_index = (5 * game->rng_index + 1) & 0x1FFF;
    return RNG_DATA[game->rng_index];
}

void game_init(Game* game, GameInterface* interface) {
    memset(game, 0, sizeof(Game));
    game->interface = interface;

    game->points = 0;
    game->rng_index = 0;
    game->frame = 0;
    game->player_lives = 3;
    game->level_index = 0;
    game->fright_ticker = 0;
    game->is_retry = FALSE;
    game->ready_ticker = 0;
    game->jingle_tick = 144;
    game->player_die_ticker = 0;
    game->fruit_ticker = 0;
    
    player_init(&game->player);

    load_map(game);
}

void game_reset(Game* game, int is_retry) {
    game->is_retry = is_retry;

    if (is_retry) {
        game->player_lives--;

        // redraw pellets
        for (int x = 0; x < TILE_WIDTH; x++) {
            for (int y = 0; y < TILE_HEIGHT; y++) {
                if (game->map[x][y] == GAME_TILE_PELLET) {
                    game->interface->display_info.tiles[x][y] = CHAR_PELLET;
                } else if (game->map[x][y] == GAME_TILE_POWER_PELLET) {
                    game->interface->display_info.tiles[x][y] = CHAR_POWER_PELLET;
                }
            }
        }
    }

    game->rng_index = 0;
    game->wave = 0;
    game->wave_ticks_remaining = get_wave_length(game, game->level_index, 0);
    game->ready_ticker = 110;
    game->play_return_sound = FALSE;

    game->ghosts_eaten = 0;
    game->eaten_ghost = NULL;
    game->eating_tick = 0;    
    game->fruit_ticker = 0;

    game->fright_ticker = 0;
    game->flashes_remaining = 0;
    game->flash_ticker = 0;

    game->ghost_dot_count = 0;
    game->use_ghost_dot_count = !is_retry;
    game->ticks_since_dot_eaten = 0;

    game->player_die = FALSE;
    player_init(&game->player);

    GhostAI ghost_ais[4];
    ghost_ai_blinky(&ghost_ais[0]);
    ghost_ai_pinky(&ghost_ais[1]);
    ghost_ai_inky(&ghost_ais[2]);
    ghost_ai_clyde(&ghost_ais[3]);

    ghost_init(&game->ghosts[0], game, DIR_LEFT, ghost_ais[0]);
    ghost_init(&game->ghosts[1], game, DIR_DOWN, ghost_ais[1]);
    ghost_init(&game->ghosts[2], game, DIR_UP, ghost_ais[2]);
    ghost_init(&game->ghosts[3], game, DIR_UP, ghost_ais[3]);

    // ghost default position from ghost_init is blinky's position
    // so no need to set blinky's position
    game->ghosts[0].in_house = FALSE;
    ghost_left_house(&game->ghosts[0], game);
    game->ghosts[1].x = 14 * 8;
    game->ghosts[2].x = 12 * 8;
    game->ghosts[3].x = 16 * 8;
    game->ghosts[2].y = 17 * 8 + 4;
    game->ghosts[1].y = 17 * 8 + 4;
    game->ghosts[3].y = 17 * 8 + 4;

}

void game_free(Game* game) {
    (void)game;
}

void update_wave(Game* game) {
    if (game->wave < 7) {
        if (--game->wave_ticks_remaining == 0) {
            // even waves are Scatter waves
            // odd waves are Chase waves
            game->wave++;
            int scatter = game->wave % 2 == 0;

            printf("set scatter to %s\n", scatter ? "true" : "false");
            for (int i = 0; i < 4; i++) {
                ghost_set_scatter(&game->ghosts[i], scatter);
            }

            // reset wave ticks
            game->wave_ticks_remaining = get_wave_length(game, game->level_index, game->wave);
        }
    }
}

void game_tick(Game *game) {
    // update inputs
    player_update_inputs(&game->player, game);
    if ((game->interface->inputs & INPUT_DEBUG) && !(game->interface->last_inputs & INPUT_DEBUG)) {
        game->debug = !game->debug;
    }

    if (game->jingle_tick > 0) {
        game->frame++;

        game->jingle_tick--;
        if (game->jingle_tick == 0)
            game->player_lives--;
        
        return;
    }

    if (game->restart) {
        game_reset(game, TRUE);
        game->restart = FALSE;
    }

    if (game->ready_ticker > 0) {
        game->ready_ticker--;
        game->frame++;

        if (game->ready_ticker == 0) {
            sound_start(&game->interface->sound, SOUND_BACKGROUND);
        }

        return;
    }

    if (game->player_die) {
        if (--game->player_die_ticker == 0) {
            player_begin_die_animation(&game->player);
        }

        if (game->player_die_ticker == -28) {
            sound_start_dump(&game->interface->sound, 1, SOUND_DEAD, sizeof(SOUND_DEAD)/ sizeof(*SOUND_DEAD));
        }

        if (game->player_die_ticker == -150) {
            game->restart = TRUE;

            // for some reason the original game clears the pellets the frame
            // before it restarts. not that hard to implement, so why not
            for (int x = 0; x < TILE_WIDTH; x++) {
                for (int y = 0; y < TILE_HEIGHT; y++) {
                    if (game->map[x][y] == GAME_TILE_PELLET || game->map[x][y] == GAME_TILE_POWER_PELLET) {
                        game->interface->display_info.tiles[x][y] = CHAR_EMPTY;
                    }
                }
            }
        }
    }

    player_update(&game->player, game);

    for (int i = 0; i < 4; i++) {
        ghost_update(&game->ghosts[i], game);
    }

    // show fruit if 70 or 170 dots have been eaten
    if (game->dots_left == 176 || game->dots_left == 76) {
        game->fruit_ticker = 9 * 60 + game_get_random_number(game) % 60;
    }

    // update fruit
    if (game->fruit_ticker > 0) {
        game->fruit_ticker--;

        // if player is touching fruit, NOM NOM
        if (obj_cell_x(&game->player) == 14 && obj_cell_y(&game->player) == 20) {
            game->points += BONUS_DATA[LEVEL_SPECS[game->level_index].bonus][1];
            game->fruit_ticker = 0;
            game->fruit_point_display_timer = 60;
            sound_start(&game->interface->sound, SOUND_EAT_FRUIT);

            // TODO: show points in tilemap
        }
    }

    if (game->eating_tick > 0) {
        if (--game->eating_tick == 0) game->eaten_ghost = NULL;
    } else {
        // play return sound if a ghost is being eaten
        int play_return_sound = FALSE;

        for (int i = 0; i < 4; i++) {
            if (game->ghosts[i].eaten) {
                play_return_sound = TRUE;
                break;
            }
        }

        if (play_return_sound != game->play_return_sound) {
            game->play_return_sound = play_return_sound;

            if (play_return_sound)
                sound_start(&game->interface->sound, SOUND_GHOST_RETURN);
            else
                sound_stop(&game->interface->sound, SOUND_GHOST_RETURN);
        }

        game->ticks_since_dot_eaten++;

        // make a ghost leave the house
        // if enough time has passed since
        // a dot was last eaten
        int time_limit = 4 * 60;
        if (game->level_index >= 4) time_limit = 3 * 60;
        if (game->ticks_since_dot_eaten >= time_limit) {
            for (int i = 0; i < 4; i++) {
                if (game->ghosts[i].in_house) {
                    ghost_leave_house(&game->ghosts[i]);
                    game->ticks_since_dot_eaten = 0;
                    break;
                }
            }
        }

        // decrease FrightTicker, and if it reaches 0
        // defrighten all the ghosts
        if (game->fright_ticker > 0 && --game->fright_ticker == 0) {
            game->flashes_remaining = LEVEL_SPECS[game->level_index].flash_count + 1;
            game->flash_ticker = 0;
        }

        if (game->flashes_remaining > 0) {
            if (--game->flash_ticker <= 0) {
                game->flash_ticker = 30;
                if (--game->flashes_remaining == 0) {
                    for (int i = 0; i < 4; i++) {
                        ghost_defrighten(&game->ghosts[i]);
                    }

                    sound_stop(&game->interface->sound, SOUND_FRIGHTENED);
                }
            }
        }

        if (game->fright_ticker == 0 && game->flashes_remaining == 0)
            update_wave(game);
    }

    game->frame++;
}

uint8_t game_get_tile(Game* game, int x, int y) {
    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x >= TILE_WIDTH) x = TILE_WIDTH - 1;
    if (y >= TILE_HEIGHT) y = TILE_HEIGHT - 1;

    return game->map[x][y];
}

void game_set_tile(Game *game, int x, int y, uint8_t value) {
    if (x < 0 || y < 0 || x >= TILE_WIDTH || y >= TILE_HEIGHT) return;
    game->map[x][y] = value;

    if (value == GAME_TILE_EMPTY) {
        game->interface->display_info.tiles[x][y] = CHAR_EMPTY;
    }
}

int game_is_tile_solid(Game *game, int x, int y) {
    uint8_t tile = game_get_tile(game, x, y);
    return tile == GAME_TILE_SOLID;
}

void game_player_ate_dot(Game* game) {
    game->points += 10;
    game->dots_left--;
    game->ticks_since_dot_eaten = 0;

    if (game->use_ghost_dot_count) {
        for (int i = 0; i < 4; i++) {
            if (game->ghosts[i].in_house) {
                game->ghosts[i].dot_counter++;
                break;
            }
        }
    } else {
        game->ghost_dot_count++;

        // if clyde is in house and counter is 32,
        // disable gloabl counter
        // (ghosts[3] is always Clyde)
        if (game->ghosts[3].in_house && game->ghost_dot_count == 32) {
            game->use_ghost_dot_count = TRUE;
            game->ghost_dot_count = 0;
        }
    }
}

void game_player_ate_ghost(Game* game, Ghost* ghost) {
    // 2^ghostsEaten * 200
    int points_gained = (1 << game->ghosts_eaten) * 200;

    game->points += points_gained;
    game->ghosts_eaten++;
    game->eating_tick = 60;
    game->eaten_ghost = ghost;
}

void game_player_ate_power_pellet(Game* game) {
    game->points += 50;
    game->ghosts_eaten = 0;

    game->fright_ticker = LEVEL_SPECS[game->level_index].fright_secs * 60;
    game->flashes_remaining = 0;
    game->flash_ticker = 0;

    for (int i = 0; i < 4; i++) {
        ghost_frighten(&game->ghosts[i]);
    }

    if (game->fright_ticker == 0) {
        for (int i = 0; i < 4; i++) {
            ghost_defrighten(&game->ghosts[i]);
        }   
    } else {
        sound_stop(&game->interface->sound, SOUND_FRIGHTENED);
        sound_start(&game->interface->sound, SOUND_FRIGHTENED);
    }
}

void game_player_ghost_touch_player(Game* game) {
    sound_stop_all(&game->interface->sound);

    game->player_die = TRUE;
    game->player_die_ticker = 60;
}

void game_draw(Game* game, DisplayUtilState* render) {
    if (game->jingle_tick == 0) {
        // draw fruit
        if (game->fruit_ticker > 0) {
            render->flip_x = FALSE;
            render->flip_y = FALSE;
            render->palette = BONUS_DATA[LEVEL_SPECS[game->level_index].bonus][0];
            display_sprite(render, 14 * 8 - 4, 20 * 8 - 4, LEVEL_SPECS[game->level_index].bonus);
        }

        if (game->eaten_ghost == NULL) {
            player_draw(&game->player, render);
        }

        if (!game->player_die || game->player_die_ticker > 0) {
            for (int i = 0; i < 4; i++) {
                if (game->ghosts+i == game->eaten_ghost) continue;
                ghost_draw(&game->ghosts[i], game, render);
            }
        }

        // draw points graphic when eating a ghost
        if (game->eaten_ghost) {
            render->flip_x = FALSE;
            render->flip_y = FALSE;
            render->palette = 24;
            display_sprite(
                render,
                game->eaten_ghost->x - 8, game->eaten_ghost->y - 8,
                SPRITE_PTS + (game->ghosts_eaten - 1)
            );
        }
    }

    // ready text
    display_clear_rect(render, 11, 20, 6, 1);
    if (game->ready_ticker > 0) {
        render->palette = 9;
        display_text(render, 11, 20, "ready!");
    }

    // clear display rows
    display_clear_row(render, 0);
    display_clear_row(render, 1);
    display_clear_row(render, 2);
    display_clear_row(render, 34);
    display_clear_row(render, 35);

    // score display
    render->palette = 31;

    if (game->frame % 30 <= 15) {
        display_text(render, 3, 0, "1up");
    }

    // blink power pellets, cycle of 18 frames
    if (game->ready_ticker == 0) {
        int show = -1;

        if (game->frame % 20 == 0) {
            show = TRUE;    
        }

        if (game->frame % 20 == 10) {
            show = FALSE;
        }

        if (show != -1) {
            for (int x = 0; x < TILE_WIDTH; x++) {
                for (int y = 0; y < TILE_HEIGHT; y++) {
                    if (game->map[x][y] == GAME_TILE_POWER_PELLET) {
                        game->interface->display_info.tiles[x][y] = show ? CHAR_POWER_PELLET : CHAR_EMPTY;
                    }
                }
            }
        }
    }

    char strbuf[32];
    sprintf(strbuf, "%02i", game->points);
    display_text(render, (7 - strlen(strbuf)), 1, strbuf);

    // high score display
    // TODO: save high score
    display_text(render, 9, 0, "high score");
    display_text(render, (17 - strlen(strbuf)), 1, strbuf);

    // lives display
    {
        int draw_x = 2;
        int draw_y = 34;

        for (int i = 0; i < game->player_lives; i++) {
            render->interface->display_info.tiles[draw_x+1][draw_y] = 32;
            render->interface->display_info.tiles[draw_x][draw_y] = 33;
            render->interface->display_info.tiles[draw_x+1][draw_y+1] = 34;
            render->interface->display_info.tiles[draw_x][draw_y+1] = 35;

            render->interface->display_info.tile_palettes[draw_x+1][draw_y] = 9;
            render->interface->display_info.tile_palettes[draw_x][draw_y] = 9;
            render->interface->display_info.tile_palettes[draw_x+1][draw_y+1] = 9;
            render->interface->display_info.tile_palettes[draw_x][draw_y+1] = 9;

            draw_x += 2;
        }
    }

    // debug info
    if (game->debug) {
        char buf[128];
        render->palette = 31;

        sprintf(buf, "lvl %i phase %i %s %i", game->level_index, game->wave, game->wave % 2 == 0 ? "scat" : "chse", game->wave_ticks_remaining / 60);
        display_text(render, 0, 2, buf);

        sprintf(buf, "dots %i", game->dots_left);
        display_text(render, 0, 3, buf);

        for (int i = 0; i < 2; i++) {
            Ghost* ghost = &game->ghosts[(game->frame * 2 + i) % 4];
            if (ghost->fright) continue;

            render->flip_x = FALSE;
            render->flip_y = FALSE;
            render->palette = ghost->ai.palette;
            display_sprite(render, ghost->ai.target_x * 8 - 4, ghost->ai.target_y * 8 - 4, 8);
        }
    }
}

int game_ghost_flash(Game* game) {
    return game->flashes_remaining > 0 && game->flash_ticker <= 15;
}

int game_player_has_power_pill(Game* game) {
    return game->fright_ticker > 0 && game->flashes_remaining > 0;
}